Skip to content

feat(connections): edit a SQLite database on an SSH server in place - #2835

Merged
datlechin merged 2 commits into
mainfrom
feat/remote-sqlite-on-server
Sep 14, 2026
Merged

datlechin merged 2 commits into
mainfrom
feat/remote-sqlite-on-server

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2831.

What this adds

A SQLite Remote Database File connection gains an Open menu beside the path: On the Server (new) or As a Read-Only Copy (today's behaviour). On the Server runs your statements on the SSH host against the live database, so the sidebar, grid, SQL editor and structure edits all read and write the file the server's own programs are using.

New connections default to On the Server; saved connections keep the read-only copy.

Why it works this way

SQLite's file locking is only reliable for a process on the same machine as the file (sqlite.org/useovernet.html), so a client that wrote across the network would corrupt the database. The only safe way to edit a live remote SQLite database is to run the SQL on the server. Measured across the runtime matrix, no mainstream desktop client does this today: TablePlus, DataGrip and DBeaver either open a read-only copy or decline.

How it works

  • The app sends a small Python program over the SSH connection (base64 on the exec command line, nothing installed, no server port). The program opens the database with the server's own libsqlite3 through ctypes and speaks a length-prefixed binary protocol on stdio.
  • The transport is a per-connection loopback listener modeled on the existing SSH tunnel (RemoteSQLiteTransportManager / RemoteSQLiteTunnel). Each accepted client presents a random per-connection token, then gets a fresh exec channel; the SQLite plugin dials the loopback port as if it were a database server.
  • The SQLite plugin's execution is split behind SQLiteExecutionBackend: the existing local sqlite3 path and a new agent backend that talks to the loopback socket. Every metadata, structure and write path is unchanged; it just runs over whichever backend the connection selected.
  • The connection reuses the existing SSH stack whole: password, key, agent, keyboard-interactive, jump hosts, TOTP, and host-key verification.

Nothing installed on the server, no port opened, no new credential. The server needs python3 3.6+, which every mainstream cloud image already ships; when it is missing, the connection fails inline and names the read-only copy as the fallback.

Runtime facts, measured

Verified in Docker across the target matrix (scripts committed under the branch's investigation, results in the PR discussion):

  • SQLite 3.26 (Rocky 8, Python 3.6) through 3.54 (macOS): the agent loads libsqlite3, and typed values, declared types, 64-bit integers, blobs, the busy timeout, query cancel (interrupt + reusable connection) and row counts all match the local driver.
  • python3 with its standard library is present by default on Ubuntu/Debian/RHEL cloud images; the sqlite3 CLI is not, which is why the agent uses libsqlite3 via ctypes rather than the CLI.

Security

  • The Python agent installs a sqlite3 authorizer denying fts3_tokenizer and load_extension. fts3_tokenizer('name', <blob>) registers an arbitrary pointer as a tokenizer and dereferences it; measured, that segfaults the process on every server build and on the app's own libsqlite3. The same authorizer is now installed on the local SQLite backend too, closing that crash primitive for ordinary local files.
  • The agent opens READWRITE without CREATE (a mistyped path fails instead of making an empty database) and without URI mode, and never enables extension loading. It binds parameters, runs one statement per request, and self-terminates on an idle deadline so a crashed or sleeping Mac never leaves a lock on the server.
  • The loopback listener admits a client only after it presents a 256-bit random per-connection token, compared in constant time, so a local process cannot reach the server's database.
  • The exec command line is a fixed literal but for the base64 of the program; the database path and every value travel as protocol fields, never as shell text.
  • AI chat gate closed: the in-app assistant's execute_query now refuses statements that read or write files or run server-side code (ATTACH, LOAD, VACUUM INTO), matching what the MCP server already enforced through ExternalStatementGate. Without this, the AI path was a read-only-external and filesystem/code bypass, larger still on a connection that executes on an SSH server.

Tests

  • Protocol codec round-trip, including NULL vs empty text, blob bytes, 64-bit integers, chunk-boundary reassembly and an oversize-frame reject.
  • Agent over local pipes against a real database (skipped when python3 is absent): typed values and declared types match the local driver, INSERT reports its row count, fts3_tokenizer and load_extension are denied, and a missing file fails without being created. This exercises the exact Python program that ships.
  • Admission token accept/reject, the launcher builder, and the app/plugin wire-constant parity guard.
  • Model wiring: absent remoteFileAccess decodes as the read-only copy, each mode resolves to the right transport, a file-backed connection never resolves to a plain SSH tunnel, and the Read-Only Safe Mode floor applies to the copy but not the live session.

UI automation for the Open pop-up was left out: the connection form's transport pane is not driven deterministically by the existing UI suites, and the pop-up's behaviour is covered by the model and transport tests instead.

Not in this PR

DuckDB/libSQL/Beancount live mode (no standard-library runtime to open them on the server), iOS (its own SQLite and SSH stacks), and a remote file browser.

Verification

  • build (app + bundled SQLite plugin): PASS
  • test (RemoteSQLite suites + ConnectionTunnelKind, SafeModeFloor, ConnectionFormTransport, SSHConfiguration neighbours): PASS
  • lint: clean
  • docs: PASS
  • AllPlugins aggregate is red only on the pre-existing oracle-nio @TaskLocal macro issue; the SQLite plugin compiles clean.

A screenshot of the new Open pop-up in the connection form is pending and will be added to docs/connections/remote-database-files.mdx.

@mintlify

mintlify Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 14, 2026, 8:30 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit dbbac64 into main Sep 14, 2026
9 checks passed
@datlechin
datlechin deleted the feat/remote-sqlite-on-server branch September 14, 2026 09:46

This branch was successfully deployed

1 active deployment
staging - docs — 219cec22 Deployed Sep 14, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remote SQLite management

1 participant